"use strict";

var motion = motion || {};

motion.motions = [];

/*
 * call for update pending animations
 */
motion.animLastUpdate = 0;
motion.pause = false;

motion.animUpdate = function(timeStamp) {

	var timeSpan = timeStamp - motion.animLastUpdate;

	if (motion.animLastUpdate && !motion.pause) {

		if (timeSpan > 2000) {
			// time out
			console
				.log('motion.animUpdate() --> animation watchdog, skip animation step with a time span of '
					+ timeSpan + 'ms');
		} else if (timeSpan >= 20) {
			// each 20 ms
			var instances = [];
			motion.motions.foreach(function(i, value, first, last) {
				instances.push(value);
			});
			instances.foreach(function(i, value, first, last) {
				value.update(timeSpan / 1000);
			});
		} else
			return;
	}
	// remind time stamp
	motion.animLastUpdate = timeStamp;
};

motion.restore = function(backup_values) {
	var mot, mov_field;
	if (backup_values.mov_id in fields.by_id) {
		mov_field = fields.by_id[backup_values.mov_id];
		backup_values.mov_id = undefined;
		if (mov_field) {
			if (backup_values.subtype)
				// sub type of movement object
				mot = new motion[backup_values.subtype](backup_values.a, backup_values.v, mov_field, backup_values.movable_reverse);
			else
				// plain movement object
				mot = new motion.Movement(backup_values.a, backup_values.v, mov_field, backup_values.movable_reverse);
			if (backup_values.route instanceof Array) {
				// clear routes
				Array.call(this);
				// restore routes
				backup_values.route.foreach(function(i, value, first, last) {
					Array.prototype.push.call(mot, {
						from : value.from,
						to : value.to,
						from_s : value.from_s,
						to_s : value.to_s,
						field : fields.by_id[value.field_id]
					});
				});
			}
			backup_values.route = undefined;
			backup_values.a = undefined;
			backup_values.v = undefined;
			backup_values.movable_reverse = undefined;
			// append remaining values
			for ( var v in backup_values) {
				if (backup_values[v] !== undefined)
					mot[v] = backup_values[v];
			}
			// push to update list
			motion.motions.push(mot);
		}
	}
};

motion.Movement = function(a, v, mov_field, reverse) {
	if (mov_field === undefined)
		return;
	this.from_s = 0;
	this.to_s = 0; /* distance [1] */
	this.v = v || 1; /* velocity [1/s] */
	this.a = a || 1; /* acceleration [1/s²] */
	this.pos = 0; /* [1] */
	this.breakPos = 0; /* [1] */
	this.mov_field = undefined;
	this.target = 0; /* [1] */
	this.speed = 0;
	this.movable_reverse = reverse || false;
	this.setMovable(mov_field);
	this.active_idx = null;
	this.onstop = undefined;
};
motion.Movement.prototype = new Array();
motion.Movement.prototype.backup = function() {
	var route = [];
	return {
		mov_id : this.mov_field.id,
		route : this.foreach(function(i, value, first, last) {
			route.push({
				field_id : value.field.id,
				from : value.from,
				to : value.to,
				from_s : value.from_s,
				to_s : value.to_s
			});
			if (last)
				return route;
		}),
		from_s : this.from_s,
		to_s : this.to_s,
		target : this.target,
		pos : this.pos,
		v : this.v,
		a : this.a,
		movable_reverse : this.movable_reverse,
		speed : this.speed,
		active : this.active,
		active_idx : this.active_idx,
		moving : this.moving,
		subtype : this.subtype
	};
};
motion.Movement.prototype.active = function() {
	var active = motion.motions.indexOf(this) >= 0;
	return active;
};
motion.Movement.prototype.setMovable = function(mov_field) {
	if (!mov_field instanceof movable.Movable || !mov_field.placed()) { throw new Error('No movable or movable not placed!'); }
	Array.call(this);
	if (this.mov_field !== undefined
		&& this.mov_field instanceof movable.Movable) {
		this.mov_field.motion = undefined;
		this.mov_field.speed = 0;
	}
	this.mov_field = mov_field;
	this.mov_field.motion = this;
	this.speed = mov_field.speed || 0;
	var from, to, field = this.mov_field.getCurrent();
	this.from_s = 0;
	this.to_s = 0;
	if (this.movable_reverse) {
		from = this.mov_field.toDirection;
		to = this.mov_field.fromDirection;
		this.pos = field.getDistance(from, to) - this.mov_field.distance;
	} else {
		to = this.mov_field.toDirection;
		from = this.mov_field.fromDirection;
		this.pos = this.mov_field.distance;
	}
	// append field
	Array.prototype.push.call(this, {
		field : field,
		from : from,
		to : to,
		from_s : this.to_s,
		to_s : this.to_s += field.getDistance(from, to)
	});
	this.active_idx = this.length - 1;
};
motion.Movement.prototype.toDir = function() {
	var value = this.last();
	if (!value) { return this.movable_reverse ? this.mov_field.fromDirection : this.mov_field.toDirection; }
	return value.to;
};
motion.Movement.prototype.fromDir = function() {
	var value = this.last();
	if (!value) { return this.movable_reverse ? this.mov_field.toDirection : this.mov_field.fromDirection; }
	return value.from;
};
motion.Movement.prototype.nextFromDir = function() {
	return defs.CounterDirection[this.toDir()];
};
motion.Movement.prototype.nextField = function() {
	// get the next field
	var field = null;
	if (this.length > 0) {
		var last = this.last();
		field = last.field.getNext(last.to);
	} else {
		field = this.movable_reverse ? this.mov_field.getPrevius() : this.mov_field
			.getNext();
	}
	return field;
};
motion.Movement.prototype.nextToDirs = function() {
	var field = this.nextField();
	if (field)
		return field.getToDirs(this.nextFromDir());
	return defs.Direction.NONE;
};
motion.Movement.prototype.push = function(next_to_dir) {
	if (typeof next_to_dir === 'number') {
		var to = Math.abs(next_to_dir % 5);
		if (to !== defs.Direction.NONE) {
			var from, field;
			from = this.nextFromDir();
			field = this.nextField();
			// append field
			Array.prototype.push.call(this, {
				field : field,
				from : from,
				to : to,
				from_s : this.to_s,
				to_s : this.to_s += field.getDistance(from, to)
			});
			// summarize distance
			return true;
		} else {
			console
				.err('motion.Movement.prototype.push() --> Can not add field, wrong direction '
					+ arguments);
			return false;
		}
	} else {
		console
			.err('motion.Movement.prototype.push() --> Can not add field, wrong arguments '
				+ arguments);
		return false;
	}
};
motion.Movement.prototype.requestUpdate = function() {
	if (motion.motions.indexOf(this) < 0) {
		motion.motions.push(this);
	}
};
motion.Movement.prototype.moveRel = function(s) {
	this.target = Math.min(this.to_s, Math
		.max(this.from_s, this.pos + (s || 0)));
	this.requestUpdate();
};
motion.Movement.prototype.moveAbs = function(s) {
	this.target = Math.min(this.to_s, Math.max(this.pos, s || 0));
	this.requestUpdate();
};
motion.Movement.prototype.moveField = function(index) {
	var fieldEntry = this[index];
	if (fieldEntry) {
		var fieldCenter = (fieldEntry.to_s - fieldEntry.from_s) / 2;
		this.target = Math.min(this.to_s, Math.max(this.pos, fieldEntry.from_s
			+ fieldCenter));
		this.requestUpdate();
		return true;
	}
	return false;
};
motion.Movement.prototype.stop = function() {
	this.target = this.pos;
};
motion.Movement.prototype.interrupt = function() {
	this.stop();
	this.speed = 0;
	this.mov_field.speed = 0;
};
motion.Movement.prototype.remove = function() {
	this.interrupt();
	// clear onstop callback
	this.onstop = undefined;
	// stop updates
	motion.motions.remove(this);
};
motion.Movement.prototype.indexOf = function(position) {
	return this.foreach(function(i, value, first, last) {
		if (value.from_s <= position && position <= value.to_s)
			return i;
		if (last)
			return -1;
	});
};
motion.Movement.prototype.maxTargetPos = function() {
	var result = this.from_s;
	var active_idx = (this.active_idx + 1) || this.indexOf(this.pos);
	for (var i = active_idx; i >= 0 && i < this.length; ++i) {
		var value = this[i];
		if (!value.field.canMove(value.from, value.to))
			return value.from_s;
		result = value.to_s;
	}
	return result;
};
motion.Movement.prototype.update = function(secs) {
	if (!this.mov_field.placed()) {
		this.remove();
		return;
	}
	this.v = this.v || 1;
	this.a = this.a || 1;
	var v_diff = this.a * secs;
	var s = (this.target - this.pos);
	var lastSpeed = this.speed;
	var bdist = Math.abs(this.a * Math.pow(this.speed / this.a, 2) / 2);
	var breakPos = this.target - bdist;
	var bpos = (this.pos >= breakPos);

	if (secs > 0) {
		if (s > 0 || this.speed > 0) {

			if (bpos || s <= 0) {
				// break
				this.speed = Math.max(0, this.speed - v_diff);
			} else {
				// move to target
				if (this.speed.fuzzyEquals(this.v)) {
					// constant movement
					this.speed = this.v;
				} else if (this.speed < this.v) {
					// acceleration
					this.speed = Math.min(this.v, this.speed + v_diff);
				} else {
					// deceleration
					this.speed = Math.max(this.v, this.speed - v_diff);
				}
			}
			// calculate current position
			if (this.speed.fuzzyIsNull() && lastSpeed === 0) {
				// has stopped
				var onStop = this.onstop;
				this.remove();
				if (typeof onStop === 'function') {
					// callback
					onStop(this.mov_field);
				}
			} else {
				var newPos = this.pos + this.speed * secs;
				this.pos = Math.min(this.to_s, Math.max(this.from_s, newPos));
			}
		} else {
			this.speed = 0;
			this.target = this.pos;
			// stop updates
			var onStop = this.onstop;
			this.remove();
			if (typeof onStop === 'function') {
				// callback
				onStop(this.mov_field);
			}
		}
	}

	// get the matching field
	var active_idx = this.indexOf(this.pos);
	var activeField = this[active_idx];

	if (!activeField) {
		console
			.log('motion.Movement.prototype.update() --> can not determine the field of position '
				+ this.pos);
		this.mov_field.speed = this.speed = 0;
		this.target = this.pos;
		this.active_idx = null;
		// stop updates
		motion.motions.remove(this);
		return false;
	}
	this.active_idx = active_idx;

	// update movable
	this.mov_field.speed = this.speed * (this.movable_reverse ? -1 : 1);

	// check if movable placed on the same field
	var from, to, distance;
	// turn moving parameter according to movable field direction (maybe moving
	// backward)
	if (this.movable_reverse) {
		from = activeField.to;
		to = activeField.from;
		distance = activeField.to_s - this.pos;
	} else {
		from = activeField.from;
		to = activeField.to;
		distance = this.pos - activeField.from_s;
	}
	if (!activeField.field.equals(this.mov_field)) {
		this.mov_field
			.replace(activeField.field.column, activeField.field.row, from, to, distance);
	} else {
		this.mov_field.applyDistance(distance);
	}
	return true;
};

motion.VelocityMovement = function(a, v, mov_field, reverse, forerun, fn_callback_req_dir, context) {
	motion.Movement.call(this, a, v, mov_field, reverse);
	this.moving = false;
	this.fn_callback_context = context || this;
	this.fn_callback_req_next = fn_callback_req_dir
		|| function(from_dir, to_dirs, field, mov_field) {
			var movableDirs = [];
			to_dirs.foreach(function(i, value, first, last) {
				if (field.canMove(from_dir, value))
					movableDirs.push(value);
			});
			if (movableDirs.length === 0)
				// all directions blocked, choose a blocked one
				movableDirs = to_dirs;
			if (movableDirs.length > 0) {
				// more then one direction
				return movableDirs[Math.floor(Math.random()
					* movableDirs.length)];
			}
		};
	this.forerun = forerun || 0;
	this.subtype = 'VelocityMovement';
};
motion.VelocityMovement.prototype = new motion.Movement();
motion.VelocityMovement.prototype.setReqDirCallback = function(fn, context) {
	this.fn_callback_req_next = fn || this.fn_callback_req_next;
	this.fn_callback_context = context || this;
};
motion.VelocityMovement.prototype.update = function(secs) {
	// append new fields
	if (this.active_idx >= this.length - 1 - this.forerun) {
		var toDirs = this.nextToDirs();
		if (toDirs.length > 0) {
			var to = this.fn_callback_req_next
				.call(this.fn_callback_context, this.nextFromDir(), toDirs, this
					.nextField(), this.mov_field);
			if (toDirs.indexOf(to) >= 0) {
				this.push(to);
			} else if (to !== null && to !== undefined)
				console
					.log('motion.VelocityMovement.prototype.update() --> callback (fn_callback_req_dir) has a wrong result: '
						+ to
						+ ', expected a number of '
						+ toDirs
						+ ' or null/undefined instead (function: '
						+ this.fn_callback_req_next + ')');
			else
				// stop when no new direction was added
				this.moving = false;
		} else
			this.moving = false;
	}
	// remove passed fields
	if (this.active_idx >= 1 && this.moving) {
		this.splice(0, this.active_idx);
		this.active_idx = 0;
		this.from_s = this[0].from_s;
	}
	if (this.moving)
		// keep moving alive
		this.move();
	// call super update function
	motion.Movement.prototype.update.call(this, secs);
};
motion.VelocityMovement.prototype.move = function() {
	this.moving = true;
	this.moveAbs(this.maxTargetPos() - 0.5);
};
motion.VelocityMovement.prototype.stop = function() {
	this.moving = false;
	motion.Movement.prototype.stop.call(this);
};
